/*
 * Test priv_suspend
 *
 * Copyright 2022 and 2023 Odin Kroeger.
 *
 * This file is part of suCGI.
 *
 * suCGI is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Affero General Public License as published
 * by the Free Software Foundation, either version 3 of the License,
 * or (at your option) any later version.
 *
 * suCGI is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
 * Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public
 * License along with suCGI. If not, see <https://www.gnu.org/licenses/>.
 */

#define _XOPEN_SOURCE 700

#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>
#include <err.h>
#include <errno.h>
#include <inttypes.h>
#include <pwd.h>
#include <setjmp.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "../attr.h"
#include "../macros.h"
#include "../params.h"
#include "../priv.h"
#include "types.h"
#include "libutil/abort.h"
#include "libutil/user.h"
#include "libutil/types.h"


/*
 * Constants
 */

/* Function signature format */
#define FNSIG "<ruid>=%lld <euid>=%lld --> ()"


/*
 * Data types
 */

/* Combinations of effective and real process UIDs */
typedef struct {
    uid_t ruid;
    uid_t euid;
} PrivSuspendArgs;


/*
 * Main
 */

int
main(void)
{
    /* User ID of the superuser */
    const uid_t superuid = 0;

    /* User ID that only a regular user would have */
    uid_t reguid = START_UID;

    if (getuid() == 0) {
        struct passwd reguser;

        if (user_get_regular(&reguser, err) == 0) {
            reguid = reguser.pw_uid;
        }
    } else {
        reguid = getuid();
    }

    /* Test cases */
    const PrivSuspendArgs cases[] = {
        {superuid, superuid},
        {superuid, reguid},
        {reguid, superuid},
        {reguid, reguid}
    };

    /* Test result */
    volatile int result = PASS;

    openlog("priv_suspend", LOG_CONS | LOG_PERROR, LOG_USER);

    for (volatile size_t i = 0; i < NELEMS(cases); ++i) {
        const PrivSuspendArgs args = cases[i];

        gid_t rgid;
        if (user_get_group(args.ruid, &rgid, err) != 0) {
            errx(ERROR, "UID %lld: no such user", (long long) args.ruid);
        }

        if (geteuid() == 0) {
            gid_t egid;
            if (user_get_group(args.euid, &egid, err) != 0) {
                errx(ERROR, "UID %lld: no such user", (long long) args.euid);
            }

            errno = 0;
            if (setregid(rgid, egid) != 0) {
                err(ERROR, "setregid");
            }

            if (setreuid(args.ruid, args.euid) != 0) {
                err(ERROR, "setreuid");
            }
        } else {
            if (getuid() != args.ruid || geteuid() != args.euid) {
                /* cppcheck-suppress misra-c2012-10.4; false positive */
                if (result == PASS) {
                    result = SKIP;
                }

                warnx("skipping " FNSIG " ...",
                      (long long) args.ruid, (long long) args.euid);
                continue;
            }
        }

        if (sigsetjmp(abort_env, 1) == 0) {
            const bool issuper = geteuid() == 0;

            (void) abort_catch(err);
            const Error retval = priv_suspend();
            (void) abort_reset(err);

            if (retval != OK) {
                result = FAIL;
                warnx(FNSIG " -> %u [!]",
                      (long long) args.ruid,
                      (long long) args.euid,
                      retval);
            }

            if (getuid() != args.ruid) {
                result = FAIL;
                warnx(FNSIG " --> <ruid> = %lld [!]",
                      (long long) args.ruid,
                      (long long) args.euid,
                      (long long) getuid());
            }

            if (geteuid() != args.ruid) {
                result = FAIL;
                warnx(FNSIG " --> <euid> = %lld [!]",
                      (long long) args.ruid,
                      (long long) args.euid,
                      (long long) geteuid());
            }

            if (getgid() != rgid) {
                result = FAIL;
                warnx(FNSIG " --> <rgid> = %lld [!]",
                      (long long) args.ruid,
                      (long long) args.euid,
                      (long long) getgid());
            }

            if (getegid() != rgid) {
                result = FAIL;
                warnx(FNSIG " --> <egid> = %lld [!]",
                      (long long) args.ruid,
                      (long long) args.euid,
                      (long long) getegid());
            }

            if (issuper) {
                gid_t groups[MAX_NGROUPS];
                errno = 0;
                const int ngroups = getgroups(MAX_NGROUPS, groups);
                if (ngroups == -1) {
                    err(ERROR, "getgroups");
                }

                if (ngroups != 1) {
                    result = FAIL;
                    warnx(FNSIG " --> <ngroups> = %d [!]",
                          (long long) args.ruid,
                          (long long) args.euid,
                          ngroups);
                }

                if (groups[0] != rgid) {
                    result = FAIL;
                    warnx(FNSIG " --> <groups[0]> = %lld [!]",
                          (long long) args.ruid,
                          (long long) args.euid,
                          (long long) groups[0]);
                }

                errno = 0;
                if (setuid(0) != 0) {
                    err(ERROR, "setuid");
                }
            }
        }

        if (abort_signal != 0) {
            result = FAIL;
            warnx(FNSIG " ^ %s [!]",
                  (long long) args.ruid,
                  (long long) args.euid,
                  strsignal((int) abort_signal));
        }
    }

    return result;
}
